/*
    Copyright (C) 2021 Huawei Device Co., Ltd.
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
 */

package com.danikula.videocache.slicingStrategy;

import com.danikula.videocache.util.LogUtil;

import ohos.agp.utils.TextTool;

import java.io.File;
import java.io.IOException;
import java.util.*;

import static com.danikula.videocache.Constants.TEMP_POSTFIX;

/**
 * 缓存文件碎片以缓存起点为名 以download结尾
 * 方便统计缓存文件的起止点以及合并
 */
public class SlicingFileUtil {
    private static final String TAG = "DivideFileUtil";
    private static List<long[]> startList = new ArrayList<>();

    /**
     * 获取碎片文件文件夹路径
     *
     * @param url      视频文件url
     * @param rootPath 缓存文件根路径
     * @return 碎片文件文件夹路径
     */
    public static String getNewRootPath(String url, String rootPath) {
        if (TextTool.isNullOrEmpty(rootPath)) {
            return "";
        }
        int lastIndex = rootPath.contains("/") ? rootPath.lastIndexOf("/") : rootPath.length();
        String oldRootPath = rootPath.substring(0, lastIndex);
        return oldRootPath + "/" + url;
    }

    /**
     * 根据拖动点获取即将使用的碎片文件
     *
     * @param offset           视频拖动位置
     * @param url              视频文件url
     * @param rootPath         缓存文件根路径
     * @param minFileCacheSize 离视频拖动点(缓存起点)最近  且它的缓存结束点小于拖动点的碎片文件，
     *                         缓存起点离它的最小距离 (小于该距离则使用小于该点最近的那个缓存文件 避免过多碎片文件以及生成缓存内容很少的碎片造成读取卡顿)
     * @param fileLength       视频文件长度
     * @return 即将使用的碎片文件
     */
    public static Optional<File> getCurrentFile(long offset, String url, String rootPath, long minFileCacheSize,
                                                long fileLength) {
        Optional<File> newFile = Optional.empty();
        try {
            if (offset < 0 || minFileCacheSize < 0) {
                return Optional.empty();
            }
            String newRootPath = getNewRootPath(url, rootPath);
            File file = createDirectory(newRootPath);
            File[] files = file.listFiles();
            boolean isOffsetCloseEnd = checkIsCloseEnd(fileLength, offset, minFileCacheSize);
            /**
             * 1. 最开始没有碎片文件 初始化一个开头为offset的文件
             */
            if (files.length == 0) {
                if (fileLength > 0 && fileLength < minFileCacheSize) {
                    /**
                     * 视频总时长很小 甚至小于最小缓存长度
                     * 只需生成一个文件就行
                     * 起始点直接为0
                     */
                    return createSingleFile(newRootPath);
                }

                if (isOffsetCloseEnd) {
                    /**
                     * 起始点在视频文件结尾 直接以距离终点minFileCacheSize的大小创建一个minFileCacheSize大小的文件
                     *  起点为fileLength-minFileCacheSize
                     *  这是防止在最开始看视频的时候直接拖动到了最后面 会生成很小的碎片文件
                     */
                    return createVideoEndFile(fileLength, minFileCacheSize, newRootPath);
                }
                // 如果起始点不在视频文件结尾 初始化一个开头为offset的文件
                return createFileFile(newRootPath, offset);
            }
            if (files.length > 0 && isOffsetCloseEnd) {
                /**
                 * 2.已经生成过碎片文件 并且拖动点在视频文件结尾的
                 * 这个时候以真实起点去生成文件也会造成单个文件很小
                 */

                // 所以先看 如果某个碎片文件的截至位置在距离终点minFileCacheSize以内 直接使用
                long tempStart = fileLength - minFileCacheSize;
                newFile = getEndFileIfExists(tempStart, files);

                // 如果靠近文件结尾 且不是第一个生成的文件 且没有生成文件 则生成这个靠近文件结尾的文件
                // 文件起始点为fileLength - minFileCacheSize
                if (newFile == null || !newFile.isPresent() || newFile.get() == null) {
                    tempStart = resetTempStart(files, tempStart, minFileCacheSize);
                    newFile = createEndFile(newRootPath, tempStart);
                }
                return newFile;
            }
            /**
             * 3.起始点不在视频结尾
             */
            newFile = createOrSelectFileWhenOffsetInMiddle(newRootPath, files, offset, minFileCacheSize);
        } catch (IOException e) {
            LogUtil.error(TAG, e.toString());
        }
        return newFile;
    }

    private static long resetTempStart(File[] files, long tempStart, long minFileCacheSize) throws IOException {
        long start = tempStart;
        for (int i = 0; i < files.length; i++) {
            File file = files[i];
            long endIndex = changeFileToArea(file)[1];
            if (start > endIndex && start - endIndex < minFileCacheSize) {
                return endIndex;
            } else if (start < endIndex && endIndex - start < minFileCacheSize) {
                return endIndex;
            }
        }
        return tempStart;
    }

    private static Optional<File> createOrSelectFileWhenOffsetInMiddle(String newRootPath, File[] files, long offset,
                                                                       long minFileCacheSize) throws IOException {
        File newFile = null;
        boolean isNeedCreateNewFile = true;
        long tempOffset = 0;
        long currentOffset = 0;
        File tempFile = null;
        for (int i = (files.length - 1); i >= 0; i--) {
            File file = files[i];
            if (file.isFile()) {
                long startIndex = changeFileToArea(file)[0];
                long endIndex = changeFileToArea(file)[1];
                if (offset >= endIndex) {
                    currentOffset = offset - endIndex;
                }
                if (tempOffset == 0) {
                    tempOffset = currentOffset;
                }
                // a.先看起始点是否在某个碎片文件以内 若在，直接使用该碎片文件
                if (startIndex <= offset && offset < endIndex) {
                    isNeedCreateNewFile = false;
                    newFile = file;
                    break;
                } else if (offset >= endIndex && currentOffset < tempOffset) {
                    // b.再看拖动点不在某个碎片文件 找出所有碎片文件里面的结束点比起始点点小 且距离起始点最近的那个
                    tempOffset = currentOffset;
                    tempFile = file;
                } else {
                    continue;
                }
            }
        }
        // c.距离起始点最近的碎片文件 它的结束点如果和起始点的距离在minFileCacheSize以内
        // 那么直接使用它 继续缓存到它里面 尽量避免生成过多的碎片文件
        if (offset - tempOffset < minFileCacheSize && tempFile != null) {
            isNeedCreateNewFile = false;
            newFile = tempFile;
        }
        // d.处理了起始点在碎片文件以内 在碎片文件后面很近的情况 其他的均需要创建新的碎片文件
        // 哪怕起始点在某个碎片前面很近 因为下载到之后结尾会合并文件
        if (isNeedCreateNewFile) {
            newFile = createNewFile(newRootPath, offset);
        }
        return Optional.of(newFile);
    }

    private static File createNewFile(String newRootPath, long offset) throws IOException {
        String writePath = newRootPath + "/" + offset + TEMP_POSTFIX;
        File newFile = new File(writePath);
        if (!newFile.exists()) {
            boolean isCreate = newFile.createNewFile();
            LogUtil.info(TAG, "createNewFile is success :" + isCreate);
        }
        return newFile;
    }

    private static Optional<File> createEndFile(String newRootPath, long tempStart) throws IOException {
        String writePath = newRootPath + "/" + tempStart + TEMP_POSTFIX;
        File newFile = new File(writePath);
        if (!newFile.exists()) {
            boolean isCreate = newFile.createNewFile();
            LogUtil.info(TAG, "createNewFile is success :" + isCreate);
        }
        return Optional.of(newFile);
    }

    private static Optional<File> getEndFileIfExists(long tempStart, File[] files) throws IOException {
        for (int i = (files.length - 1); i >= 0; i--) {
            if (files[i].isFile()) {
                // 在距离文件结束点以内minFileCacheSize 已经存在缓存文件  直接使用
                if (tempStart == changeFileToArea(files[i])[0]) {
                    return Optional.of(files[i]);
                }
            }
        }
        return Optional.empty();
    }

    private static Optional<File> createFileFile(String newRootPath, long offset) throws IOException {
        String writePath = newRootPath + "/" + offset + TEMP_POSTFIX;
        File newFile = new File(writePath);
        if (!newFile.exists()) {
            boolean isCreate = newFile.createNewFile();
            LogUtil.info(TAG, "createNewFile is success :" + isCreate);
        }
        return Optional.of(newFile);
    }

    private static Optional<File> createSingleFile(String newRootPath) throws IOException {
        String writePath = newRootPath + "/" + 0 + TEMP_POSTFIX;
        File newFile = new File(writePath);
        if (!newFile.exists()) {
            boolean isCreate = newFile.createNewFile();
            LogUtil.info(TAG, "createNewFile is success :" + isCreate);
        }
        return Optional.of(newFile);
    }

    private static Optional<File> createVideoEndFile(long fileLength, long minFileCacheSize,
                                                     String newRootPath) throws IOException {
        long tempStart = fileLength - minFileCacheSize;
        String writePath = newRootPath + "/" + tempStart + TEMP_POSTFIX;
        File newFile = new File(writePath);
        if (!newFile.exists()) {
            boolean isCreate = newFile.createNewFile();
            LogUtil.info(TAG, "createNewFile is success : " + isCreate);
        }
        return Optional.of(newFile);
    }

    private static boolean checkIsCloseEnd(long fileLength, long offset, long minFileCacheSize) {
        return fileLength - offset < minFileCacheSize &&
                fileLength > minFileCacheSize && fileLength != Long.MAX_VALUE;
    }

    private static File createDirectory(String newRootPath) {
        File file = new File(newRootPath);
        if (!file.exists()) {
            boolean mkdirs = file.mkdirs();
            LogUtil.info(TAG, "mkdirs is success: " + mkdirs);
        }
        return file;
    }

    /**
     * 获取当前缓存文件的起点
     *
     * @param file 当前缓存文件
     * @return 当前缓存文件的起点
     */
    public static long getFileCacheStart(File file) {
        try {
            if (file == null || !file.exists()) {
                return -1;
            }
            String absolutePath = file.getCanonicalPath();
            int start = absolutePath.lastIndexOf("/");
            int end = absolutePath.lastIndexOf(".");
            String name = absolutePath.substring(start + 1, end);
            return Long.parseLong(name);
        } catch (IOException e) {
            LogUtil.error(TAG, e.toString());
        }
        return -1;
    }

    /**
     * 获取所有缓存文件的起止点
     *
     * @param url      视频url
     * @param rootPath 碎片文件夹根路径
     * @return 所有缓存文件的起止点
     */
    public static List<long[]> getFileStartList(String url, String rootPath) {
        addStartIntoList(url, rootPath);
        Collections.sort(startList, (left, right) -> (int) (left[0] - right[0]));
        return startList;
    }

    /**
     * 将所有所有缓存文件信息转化为起止点集合
     *
     * @param url      视频url
     * @param rootPath 碎片文件夹根路径
     */
    private static void addStartIntoList(String url, String rootPath) {
        try {
            if (startList == null) {
                startList = new ArrayList<>();
            }
            // 每次都清空是因为初始化碎片文件的时候终点均设为0 缓存一段时间之后终点值会更新 这里需要更新后的数据
            startList.clear();
            String newRootPath = getNewRootPath(url, rootPath);
            File file = new File(newRootPath);
            if (!file.exists()) {
                boolean mkdirs = file.mkdirs();
                LogUtil.info(TAG, "mkdirs is success: " + mkdirs);
            }
            File[] files = file.listFiles();
            if (files.length == 0) {
                startList.add(new long[]{0L, 0L});
                return;
            }
            for (int i = 0; i < files.length; i++) {
                if (!files[i].isFile()) {
                    continue;
                }
                startList.add(changeFileToArea(files[i]));
            }
        } catch (IOException e) {
            LogUtil.error(TAG, e.toString());
        }
    }

    /**
     * 获取需要被合并以及删除的碎片文件
     *
     * @param url            视频url
     * @param rootPath       碎片文件夹根路径
     * @param mergeFileStart 碎片文件起点
     * @return 获取需要被合并以及删除的碎片文件
     */
    public static Optional<File> getMergeFile(String url, String rootPath, long mergeFileStart) {
        if (TextTool.isNullOrEmpty(url) || TextTool.isNullOrEmpty(rootPath)) {
            return Optional.empty();
        }
        if (mergeFileStart <= 0 || mergeFileStart == Long.MAX_VALUE) {
            return Optional.empty();
        }

        String newRootPath = getNewRootPath(url, rootPath);
        try {
            File file = new File(newRootPath);
            if (!file.exists()) {
                boolean mkdirs = file.mkdirs();
                LogUtil.info(TAG, "mkdirs is success: " + mkdirs);
            }
            File[] files = file.listFiles();
            if (files == null || file.length() == 0) {
                return Optional.empty();
            }
            for (int i = 0; i < files.length; i++) {
                if (!files[i].isFile()) {
                    continue;
                }
                if (changeFileToArea(files[i])[0] == mergeFileStart) {
                    return Optional.of(files[i]);
                }
            }
        } catch (IOException e) {
            LogUtil.error(TAG, e.toString());
        }
        return Optional.empty();
    }

    /**
     * 单个文件缓存的太少则将他删掉 数据过少的话读取太少很容易造成播放卡顿
     *
     * @param url               视频url
     * @param rootPath          碎片文件夹根路径
     * @param minSingleFileSize 单个碎片文件最小缓存量
     * @param minFileCacheSize  距离最近的上个碎片文件的距离
     * @param fileTotalLength   整个视频文件的长度
     */
    public static void deleteFile(String url, String rootPath, long minSingleFileSize,
                                  long minFileCacheSize, long fileTotalLength) {
        try {
            // 视频总长度本来就很小 只用生成一个文件 不删除
            if (fileTotalLength < minSingleFileSize) {
                return;
            }
            String newRootPath = getNewRootPath(url, rootPath);
            File file = new File(newRootPath);
            if (!file.exists()) {
                boolean mkdirs = file.mkdirs();
                LogUtil.info(TAG, "mkdirs is success: " + mkdirs);
            }
            long endStart = 0;
            if (fileTotalLength > 0 && fileTotalLength != Long.MAX_VALUE && minFileCacheSize > 0) {
                endStart = fileTotalLength - minFileCacheSize;
            }
            File[] files = file.listFiles();
            for (int i = (files.length - 1); i >= 0; i--) {
                File childFile = files[i];
                if (!childFile.isFile()) {
                    continue;
                }
                if (childFile.length() >= minSingleFileSize) {
                    continue;
                }
                String absolutePath = childFile.getCanonicalPath();
                int start = absolutePath.lastIndexOf("/");
                int end = absolutePath.lastIndexOf(".");
                if (end <= 0) {
                    end = absolutePath.length();
                }
                String name = absolutePath.substring(start + 1, end);
                long startIndex = Long.parseLong(name);
                if (startIndex == 0) {
                    // 最开始的那个碎片文件不删除
                    continue;
                } else if (startIndex == endStart) {
                    // 保留视频结尾的碎片文件不删除  确保结尾碎片文件一定为minFileCacheSize大小
                    continue;
                } else {
                    boolean delete = childFile.delete();
                    LogUtil.info(TAG, "delete childFile is success: " + delete);
                }
            }
        } catch (IOException e) {
            LogUtil.error(TAG, e.toString());
        }
    }

    /**
     * 获取所有缓存文件的总大小
     *
     * @param url      视频url
     * @param rootPath 碎片文件夹根路径
     * @return 所有缓存文件的总大小
     */
    public static long getCacheFileSize(String url, String rootPath) {
        long totalLength = 0;
        String newRootPath = getNewRootPath(url, rootPath);
        File file = new File(newRootPath);
        if (!file.exists()) {
            boolean mkdirs = file.mkdirs();
            LogUtil.info(TAG, "mkdirs is success: " + mkdirs);
        }
        File[] files = file.listFiles();
        for (int i = 0; i < files.length; i++) {
            if (files[i].isFile()) {
                totalLength += files[i].length();
            }
        }
        return totalLength;
    }

    /**
     * 根据碎片文件的文件名获取文件的缓存起止点
     *
     * @param file 当前碎片文件
     * @return 缓存起止点
     * @throws IOException 文件处理错误
     */
    public static long[] changeFileToArea(File file) throws IOException {
        if (file == null || file.isDirectory() || !file.exists()) {
            return new long[]{0, 0};
        }
        String absolutePath = file.getCanonicalPath();
        if (TextTool.isNullOrEmpty(absolutePath)) {
            return new long[]{0, 0};
        }
        int start = absolutePath.contains("/") ? absolutePath.lastIndexOf("/") : 0;
        int end = absolutePath.contains(".") ? absolutePath.lastIndexOf(".") : absolutePath.length();
        String name = absolutePath.substring(start + 1, end);
        long startIndex = Long.parseLong(name);
        long endIndex = startIndex + file.length();
        if (startIndex < 0) {
            startIndex = 0;
        }
        return new long[]{startIndex, endIndex};
    }
}
